
#include "Common.hpp"
using namespace std;

namespace {

//-----------------------------------------------------------------------------
const char* nVertexProgramSrc =
    "!!NVvp5.0"
    NL
    NL "main:"
    NL "    MOV result.position, vertex.attrib[0];"
    NL "    RET;"
    NL "END";

string TessControlProgramSrc(int verticesOut) {
    ostringstream ss;
    ss <<
    "!!NVtcp5.0"
    NL "VERTICES_OUT " << verticesOut << ";"
    NL "FLOAT ATTRIB iPosition = vertex.in.position;"
    NL "FLOAT PARAM TessOuter = program.env[0];"
    NL "FLOAT PARAM TessInner = program.env[1];"
    NL
    NL "main:"
    NL "    INT TEMP A0;"
    NL "    MOV.S A0.x, primitive.invocation.x;"
    NL "    MOV result.position, iPosition[A0.x];"
    NL
    NL "    MOV result.patch.tessouter[0].x, TessOuter.x;"
    NL "    MOV result.patch.tessouter[1].x, TessOuter.y;"
    NL "    MOV result.patch.tessouter[2].x, TessOuter.z;"
    NL "    MOV result.patch.tessouter[3].x, TessOuter.w;"
    NL "    MOV result.patch.tessinner[0].x, TessInner.x;"
    NL "    MOV result.patch.tessinner[1].x, TessInner.y;"
    NL "    RET;"
    NL "END";
    return ss.str();
}

string TessEvaluationProgramSrc(const string& tessMode, const string& tessSpacing) {
    ostringstream ss;
    ss <<
    "!!NVtep5.0"
    NL "TESS_MODE " << tessMode << ";"
    NL "TESS_SPACING " << tessSpacing << ";"
    NL "TESS_VERTEX_ORDER CCW;"
    NL
    NL "FLOAT PARAM ModelviewProj[4] = { program.env[0..3] };"
    NL
    NL "main:"
    NL "    FLOAT TEMP p;";
    if (tessMode == "TRIANGLES") {
        ss <<
            NL "    MUL p, vertex.tesscoord.x, vertex[1].position;"
            NL "    MAD p, vertex.tesscoord.y, vertex[2].position, p;"
            NL "    MAD p, vertex.tesscoord.z, vertex[0].position, p;";
    }
    else if (tessMode == "QUADS") {
        ss <<
            NL "    FLOAT TEMP p0, p1;"
            NL "    LRP p0, vertex.tesscoord.x, vertex[1].position, vertex[0].position;"
            NL "    LRP p1, vertex.tesscoord.x, vertex[2].position, vertex[3].position;"
            NL "    LRP p, vertex.tesscoord.y, p1, p0;";
    }
    ss <<
    NL
    NL "    DP4 result.position.x, ModelviewProj[0], p;"
    NL "    DP4 result.position.y, ModelviewProj[1], p;"
    NL "    DP4 result.position.z, ModelviewProj[2], p;"
    NL "    DP4 result.position.w, ModelviewProj[3], p;"
    NL "    RET;"
    NL "END";
    return ss.str();
}

const char* nGeometryProgramSrc =
    "!!NVgp5.0"
    NL "PRIMITIVE_IN TRIANGLES;"
    NL "PRIMITIVE_OUT TRIANGLE_STRIP;"
    NL "VERTICES_OUT 3;"
    NL
    NL "main:"
    NL "    MOV result.position, vertex[0].position;"
    NL "    EMIT;"
    NL "    MOV result.position, vertex[1].position;"
    NL "    EMIT;"
    NL "    MOV result.position, vertex[2].position;"
    NL "    EMIT;"
    NL "    RET;"
    NL "END";

const char* nFragmentProgramSrc =
    "!!NVfp5.0"
    NL
    NL "main:"
    NL "    MOV result.color, { 0, 1, 0, 1 };"
    NL "    RET;"
    NL "END";
//-----------------------------------------------------------------------------
Window* nWindow;
SDL_GLContext nContext;

GLuint nProgram[3], nTCProgram[2], nTEProgram[6];
GLuint nVBO[2];
GLuint64 nVBOAddress[2];

float nTessOuter[4] = { 2.0f, 2.0f, 2.0f, 2.0f };
float nTessInner[4] = { 2.0f, 2.0f };
int nTessMode = 0, nTessSpacing = 0;

ArcBall nArcBall;
//-----------------------------------------------------------------------------
void Setup() {
    // window
    nWindow = new Window("All Program Stages", 1024, 768);

    // OpenGL context
    nContext = SDL_GL_CreateContext(nWindow->Get());
    if (!nContext) {
        throw runtime_error("[Setup] SDL_GL_CreateContext failed.");
    }

    SDL_GL_SetSwapInterval(0);

    if (glewInit() != GLEW_OK) {
        throw runtime_error("[Setup] glewInit failed.");
    }
    if (!GLEW_NV_gpu_program5) {
        throw runtime_error("[Setup] GL_NV_gpu_program5 extension is not supported.");
    }

    // programs
    glGenProgramsARB(SDL_arraysize(nProgram), nProgram);
    glGenProgramsARB(SDL_arraysize(nTCProgram), nTCProgram);
    glGenProgramsARB(SDL_arraysize(nTEProgram), nTEProgram);

    glBindProgramARB(GL_VERTEX_PROGRAM_ARB, nProgram[0]);
    LoadGLProgram(GL_VERTEX_PROGRAM_ARB, nVertexProgramSrc);

    glBindProgramARB(GL_GEOMETRY_PROGRAM_NV, nProgram[1]);
    LoadGLProgram(GL_GEOMETRY_PROGRAM_NV, nGeometryProgramSrc);

    glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, nProgram[2]);
    LoadGLProgram(GL_FRAGMENT_PROGRAM_ARB, nFragmentProgramSrc);

    glBindProgramARB(GL_TESS_CONTROL_PROGRAM_NV, nTCProgram[0]);
    LoadGLProgram(GL_TESS_CONTROL_PROGRAM_NV, TessControlProgramSrc(3).c_str());

    glBindProgramARB(GL_TESS_CONTROL_PROGRAM_NV, nTCProgram[1]);
    LoadGLProgram(GL_TESS_CONTROL_PROGRAM_NV, TessControlProgramSrc(4).c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[0]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("TRIANGLES", "EQUAL").c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[1]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("TRIANGLES", "FRACTIONAL_EVEN").c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[2]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("TRIANGLES", "FRACTIONAL_ODD").c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[3]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("QUADS", "EQUAL").c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[4]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("QUADS", "FRACTIONAL_EVEN").c_str());

    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[5]);
    LoadGLProgram(GL_TESS_EVALUATION_PROGRAM_NV, TessEvaluationProgramSrc("QUADS", "FRACTIONAL_ODD").c_str());

    
    // vbo
    const float tri[] = { -1, -1, 1, -1, 0, 1 };
    const float quad[] = { -1, -1, 1, -1, 1, 1, -1, 1 };

    glGenBuffers(2, nVBO);

    glBindBuffer(GL_ARRAY_BUFFER, nVBO[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(tri), tri, GL_STATIC_DRAW);
    glGetBufferParameterui64vNV(GL_ARRAY_BUFFER, GL_BUFFER_GPU_ADDRESS_NV, &nVBOAddress[0]);
    glMakeBufferResidentNV(GL_ARRAY_BUFFER, GL_READ_ONLY);

    glBindBuffer(GL_ARRAY_BUFFER, nVBO[1]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
    glGetBufferParameterui64vNV(GL_ARRAY_BUFFER, GL_BUFFER_GPU_ADDRESS_NV, &nVBOAddress[1]);
    glMakeBufferResidentNV(GL_ARRAY_BUFFER, GL_READ_ONLY);

    // GL state
    glEnable(GL_DEPTH_TEST);
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // init GUI
    TwInit(TW_OPENGL, NULL);
    TwWindowSize(nWindow->Width(), nWindow->Height());

    TwBar* bar = TwNewBar("Menu");
    TwDefine("Menu color='0 0 0' alpha=128 position='10 10' size='250 200'");
    TwDefine("Menu fontresizable=false resizable=false movable=false valueswidth=120");

    const TwType tessModeType = TwDefineEnumFromString("TessMode", "TRIANGLES, QUADS");
    TwAddVarRW(bar, "Mode", tessModeType, &nTessMode, "");

    const TwType tessSpacingType = TwDefineEnumFromString("TessSpacing", "EQUAL, FRACTIONAL_EVEN, FRACTIONAL_ODD");
    TwAddVarRW(bar, "Spacing", tessSpacingType, &nTessSpacing, "");
    TwAddSeparator(bar, NULL, NULL);

    TwAddVarRW(bar, "TessOuter0", TW_TYPE_FLOAT, &nTessOuter[0], "label='TessOuter[0]' step=0.1 min=1 max=64");
    TwAddVarRW(bar, "TessOuter1", TW_TYPE_FLOAT, &nTessOuter[1], "label='TessOuter[1]' step=0.1 min=1 max=64");
    TwAddVarRW(bar, "TessOuter2", TW_TYPE_FLOAT, &nTessOuter[2], "label='TessOuter[2]' step=0.1 min=1 max=64");
    TwAddVarRW(bar, "TessOuter3", TW_TYPE_FLOAT, &nTessOuter[3], "label='TessOuter[3]' step=0.1 min=1 max=64");
    TwAddSeparator(bar, NULL, NULL);

    TwAddVarRW(bar, "TessInner0", TW_TYPE_FLOAT, &nTessInner[0], "label='TessInner[0]' step=0.1 min=1 max=64");
    TwAddVarRW(bar, "TessInner1", TW_TYPE_FLOAT, &nTessInner[1], "label='TessInner[1]' step=0.1 min=1 max=64");

    nArcBall.SetWindowSize(nWindow->Width(), nWindow->Height());
}
//-----------------------------------------------------------------------------
bool ProcessEvents() {
    SDL_Event evt;
    while (SDL_PollEvent(&evt)) {
        if (evt.type == SDL_QUIT) {
            return false;
        }

        if (ProcessCommonEvent(evt) || ProcessGUIEvent(evt)) {
            continue;
        }

        switch (evt.type) {
        case SDL_MOUSEBUTTONDOWN:
            if (evt.button.button == SDL_BUTTON_LEFT) {
                nArcBall.Begin(evt.button.x, evt.button.y);
                return true;
            }
            break;

        case SDL_MOUSEBUTTONUP:
            if (evt.button.button == SDL_BUTTON_LEFT) {
                nArcBall.End();
                return true;
            }
            break;

        case SDL_MOUSEMOTION:
            nArcBall.Drag(evt.motion.x, evt.motion.y);
            break;
        }
    }

    return true;
}
//-----------------------------------------------------------------------------
void Update() {
    // clear buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnable(GL_VERTEX_PROGRAM_ARB);
    glEnable(GL_TESS_CONTROL_PROGRAM_NV);
    glEnable(GL_TESS_EVALUATION_PROGRAM_NV);
    glEnable(GL_GEOMETRY_PROGRAM_NV);
    glEnable(GL_FRAGMENT_PROGRAM_ARB);

    glBindProgramARB(GL_VERTEX_PROGRAM_ARB, nProgram[0]);
    glBindProgramARB(GL_TESS_CONTROL_PROGRAM_NV, nTCProgram[nTessMode]);
    glBindProgramARB(GL_TESS_EVALUATION_PROGRAM_NV, nTEProgram[nTessMode * 3 + nTessSpacing]);
    glBindProgramARB(GL_GEOMETRY_PROGRAM_NV, nProgram[1]);
    glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, nProgram[2]);

    // compute transform matrices
    const glm::mat4 m = nArcBall.Rotation();
    const glm::mat4 mv = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)) * m;
    const glm::mat4 mvp = glm::transpose(glm::perspective(45.0f, 1.333f, 0.1f, 10.0f) * mv);

    glProgramEnvParameters4fvEXT(GL_TESS_CONTROL_PROGRAM_NV, 0, 1, nTessOuter);
    glProgramEnvParameters4fvEXT(GL_TESS_CONTROL_PROGRAM_NV, 1, 1, nTessInner);

    glProgramEnvParameters4fvEXT(GL_TESS_EVALUATION_PROGRAM_NV, 0, 4, &mvp[0][0]);

    glEnableClientState(GL_VERTEX_ATTRIB_ARRAY_UNIFIED_NV);
    glEnableVertexAttribArray(0);
    glVertexAttribFormatNV(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float));

    if (nTessMode == 0) { // triangles
        glPatchParameteri(GL_PATCH_VERTICES, 3);
        glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 0, nVBOAddress[0], 3 * 2 * sizeof(float));
        glDrawArrays(GL_PATCHES, 0, 3);
    }
    else if (nTessMode == 1) { // quads
        glPatchParameteri(GL_PATCH_VERTICES, 4);
        glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 0, nVBOAddress[1], 4 * 2 * sizeof(float));
        glDrawArrays(GL_PATCHES, 0, 4);
    }

    // draw GUI
    glDisable(GL_VERTEX_PROGRAM_ARB);
    glDisable(GL_TESS_CONTROL_PROGRAM_NV);
    glDisable(GL_TESS_EVALUATION_PROGRAM_NV);
    glDisable(GL_GEOMETRY_PROGRAM_NV);
    glDisable(GL_FRAGMENT_PROGRAM_ARB);

    glDisableClientState(GL_VERTEX_ATTRIB_ARRAY_UNIFIED_NV);
    glDisableClientState(GL_ELEMENT_ARRAY_UNIFIED_NV);
    for (GLuint i = 0; i < 16; ++i) {
        glDisableVertexAttribArray(i);
    }
    TwDraw();

    nWindow->Swap();
}
//-----------------------------------------------------------------------------
void Run() {
    Setup();

    while (ProcessEvents()) {
        Update();
    }
}
//-----------------------------------------------------------------------------
void Cleanup() {
    glDeleteBuffers(SDL_arraysize(nVBO), nVBO);
    glDeleteProgramsARB(SDL_arraysize(nProgram), nProgram);
    glDeleteProgramsARB(SDL_arraysize(nTCProgram), nTCProgram);
    glDeleteProgramsARB(SDL_arraysize(nTEProgram), nTEProgram);

    TwTerminate();

    SDL_GL_DeleteContext(nContext);
    delete nWindow;
}
//-----------------------------------------------------------------------------

} // namespace

//-----------------------------------------------------------------------------
int main(int argc, char** argv) {
    if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO) < 0) {
        cout << "[main] SDL_Init failed." << endl;
        return 0;
    }

    try {
        Run();
    }
    catch (const exception& ex) {
        cout << ex.what() << endl;
    }
    catch (...) {
        cout << "Undefined exception." << endl;
    }

    Cleanup();
    SDL_Quit();

    return 0;
}
//-----------------------------------------------------------------------------
